Moved analytics from tools/idea Now we use extension points, and custom hooks to modify the platform's behaviour. Bug: 156497102 Test: existing Change-Id: If7a8e341fd4ec73fba59acee72f52a1c59040e3d
diff --git a/analytics/BUILD b/analytics/BUILD new file mode 100644 index 0000000..f61dc2a --- /dev/null +++ b/analytics/BUILD
@@ -0,0 +1,20 @@ +load("//tools/base/bazel:bazel.bzl", "iml_module") + +# managed by go/iml_to_build +iml_module( + name = "analytics", + srcs = ["src"], + iml_files = ["analytics.iml"], + test_srcs = ["testSrc"], + visibility = ["//visibility:public"], + # do not sort: must match IML order + deps = [ + "//tools/idea/.idea/libraries:kotlin-stdlib-jdk8", + "//tools/idea/.idea/libraries:studio-analytics-proto", + "//tools/idea/.idea/libraries:HdrHistogram", + "//tools/analytics-library/tracker:analytics-tracker[module]", + "//tools/idea/.idea/libraries:JUnit4[test]", + "//tools/idea/.idea/libraries:protobuf", + "//tools/idea/platform/core-api:intellij.platform.core[module]", + ], +) diff --git a/analytics/analytics.iml b/analytics/analytics.iml new file mode 100644 index 0000000..ee3e75a --- /dev/null +++ b/analytics/analytics.iml
@@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="kotlin-stdlib-jdk8" level="project" /> + <orderEntry type="library" name="studio-analytics-proto" level="project" /> + <orderEntry type="library" name="HdrHistogram" level="project" /> + <orderEntry type="module" module-name="analytics-tracker" /> + <orderEntry type="library" scope="TEST" name="JUnit4" level="project" /> + <orderEntry type="library" name="protobuf" level="project" /> + <orderEntry type="module" module-name="intellij.platform.core" /> + </component> +</module> \ No newline at end of file
diff --git a/analytics/src/com/android/tools/analytics/HighlightingStats.kt b/analytics/src/com/android/tools/analytics/HighlightingStats.kt new file mode 100644 index 0000000..69ca29c --- /dev/null +++ b/analytics/src/com/android/tools/analytics/HighlightingStats.kt
@@ -0,0 +1,112 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.analytics + +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.EditorFileType +import com.google.wireless.android.sdk.stats.EditorFileType.GROOVY +import com.google.wireless.android.sdk.stats.EditorFileType.JAVA +import com.google.wireless.android.sdk.stats.EditorFileType.JSON +import com.google.wireless.android.sdk.stats.EditorFileType.KOTLIN +import com.google.wireless.android.sdk.stats.EditorFileType.KOTLIN_SCRIPT +import com.google.wireless.android.sdk.stats.EditorFileType.NATIVE +import com.google.wireless.android.sdk.stats.EditorFileType.PROPERTIES +import com.google.wireless.android.sdk.stats.EditorFileType.UNKNOWN +import com.google.wireless.android.sdk.stats.EditorFileType.XML +import com.google.wireless.android.sdk.stats.EditorHighlightingStats +import com.intellij.concurrency.JobScheduler +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.BaseComponent +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFile +import org.HdrHistogram.Recorder +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +/** + * Tracks highlighting latency across file types. + * To log an [AndroidStudioEvent] with the collected data, call [reportHighlightingStats]. + */ +object HighlightingStats : BaseComponent { + private const val MAX_LATENCY_MS = 10 * 60 * 1000 // Limit latencies to 10 minutes to ensure reasonable histogram size. + + override fun initComponent() { + // Send reports hourly and on application close. + JobScheduler.getScheduler().scheduleWithFixedDelay(this::reportHighlightingStats, 1, 1, TimeUnit.HOURS) + Disposer.register(ApplicationManager.getApplication(), Disposable(this::reportHighlightingStats)) + } + + /** + * Maps file types to latency recorders. + * We use [Recorder] to allow thread-safe read access from background threads. + */ + private val latencyRecorders = ConcurrentHashMap<EditorFileType, Recorder>() + + fun recordHighlightingLatency(document: Document, latencyMs: Long) { + if (latencyMs < 0 || latencyMs > MAX_LATENCY_MS) return + val file = FileDocumentManager.getInstance().getFile(document) ?: return + val fileType = convertFileType(file) + val recorder = latencyRecorders.computeIfAbsent(fileType) { Recorder(1) } + recorder.recordValue(latencyMs) + } + + /** + * Logs an [AndroidStudioEvent] with editor highlighting stats. + * Resets statistics so that counts are not double-counted in the next report. + */ + fun reportHighlightingStats() { + val allStats = EditorHighlightingStats.newBuilder() + for ((fileType, recorder) in latencyRecorders) { + val histogram = recorder.intervalHistogram // Automatically resets statistics for this recorder. + if (histogram.totalCount == 0L) { + continue + } + val record = EditorHighlightingStats.Stats.newBuilder().also { + it.fileType = fileType + it.histogram = histogram.toProto() + } + allStats.addByFileType(record.build()) + } + + if (allStats.byFileTypeCount == 0) { + return + } + + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.EDITOR_HIGHLIGHTING_STATS + editorHighlightingStats = allStats.build() + } + ) + } + + /** Converts from file type name to proto enum value. */ + private fun convertFileType(file: VirtualFile): EditorFileType = when (file.fileType.name) { + // We use string literals here (rather than, e.g., JsonFileType.INSTANCE.name) to avoid unnecessary + // dependencies on other plugins. Fortunately, these values are extremely unlikely to change. + "JAVA" -> JAVA + "Kotlin" -> if (file.extension == "kts") KOTLIN_SCRIPT else KOTLIN + "XML" -> XML + "Groovy" -> GROOVY + "Properties" -> PROPERTIES + "JSON" -> JSON + "ObjectiveC" -> NATIVE + else -> UNKNOWN + } +} diff --git a/analytics/src/com/android/tools/analytics/HistogramUtil.kt b/analytics/src/com/android/tools/analytics/HistogramUtil.kt new file mode 100644 index 0000000..175c5c8 --- /dev/null +++ b/analytics/src/com/android/tools/analytics/HistogramUtil.kt
@@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("HistogramUtil") + +package com.android.tools.analytics + +import com.google.wireless.android.sdk.stats.HistogramBin +import org.HdrHistogram.Histogram +import org.HdrHistogram.HistogramIterationValue + +typealias HistogramProto = com.google.wireless.android.sdk.stats.Histogram + +/** + * Returns the inclusive start value for the given bin. + */ +val HistogramIterationValue.start: Long get() { + // Special case: HdrHistogram encodes the first bin as 0,0 even though it's supposed to be 0,1 + if (valueIteratedFrom == 0L && valueIteratedTo == 0L) { + return 0L + } + return valueIteratedFrom + 1 +} + +/** + * Returns the exclusive end value for the given bin. + */ +val HistogramIterationValue.end: Long get() { + return valueIteratedTo + 1 +} + +/** + * Converts a [Histogram] to a proto. + */ +fun Histogram.toProto(): HistogramProto { + val builder = HistogramProto.newBuilder() + + var total = totalCount + builder.totalCount = total + for (value in allValues()) { + if (value.countAddedInThisIterationStep > 0L) { + // HdrHistogram has a special case for it's first bin, which uses the range 0 to 0. Subsequent bins have an inclusive + // upper bound and exclusive lower bound. We use inclusive lower bounds and exclusive upper bounds in the proto, so + // need to shuffle around some indices here. + builder.addBin(HistogramBin.newBuilder() + .setStart(value.start) + .setEnd(value.end) + .setTotalSamples(total) + .setSamples(value.countAddedInThisIterationStep)) + } + total -= value.countAddedInThisIterationStep + } + + return builder.build() +} diff --git a/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt b/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt new file mode 100644 index 0000000..d83471f --- /dev/null +++ b/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt
@@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("StudioUpdateAnalyticsUtil") + +package com.android.tools.analytics + +import com.android.tools.analytics.UsageTracker +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.StudioUpdateFlowEvent + +fun logClickUpdate(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_UPDATE), newBuild) +} + +fun logClickIgnore(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_IGNORE), newBuild) +} + +fun logClickLater(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_LATER), newBuild) +} + +fun logClickAction(actionName: String, newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder() + .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_ACTION) + .setActionName(actionName), newBuild) +} + +fun logDownloadSuccess(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.PATCH_DOWNLOAD_SUCCESS), newBuild) +} + +fun logDownloadFailure(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.PATCH_DOWNLOAD_FAILURE), newBuild) +} + +fun logUpdateDialogOpenManually(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder() + .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_OPEN) + .setDialogTrigger(StudioUpdateFlowEvent.DialogTrigger.MANUAL), newBuild) +} + +fun logUpdateDialogOpenFromNotification(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder() + .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_OPEN) + .setDialogTrigger(StudioUpdateFlowEvent.DialogTrigger.NOTIFICATION), newBuild) +} + +fun logClickNotification(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.NOTIFICATION_UPDATE_LINK_CLICKED), newBuild) + +} + +fun logNotificationShown(newBuild: String) { + log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.NOTIFICATION_SHOWN), newBuild) +} + +fun log(event: StudioUpdateFlowEvent.Builder, newBuild: String) { + event.setStudioNewVersion(newBuild) + UsageTracker.log(AndroidStudioEvent.newBuilder() + .setKind(AndroidStudioEvent.EventKind.STUDIO_UPDATE_FLOW) + .setStudioUpdateFlowEvent(event.build()) + ) +} diff --git a/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt b/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt new file mode 100644 index 0000000..f25218d --- /dev/null +++ b/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt
@@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.analytics; + +import com.android.tools.analytics.toProto +import org.HdrHistogram.Histogram +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class HistogramUtilTest { + @Test + fun testBinBoundaries() { + // We repeat the test 100 times to verify bins with different numbers of digits + for (i in 0..100) { + val hist = Histogram(1) + hist.recordValue(i.toLong()) + val proto = hist.toProto() + assertEquals("There should be one sample in the histogram", 1, proto.totalCount) + val binList = proto.binList + assertEquals("Empty bins should not be converted to protos", 1, binList.size) + val bin = binList[0] + assertTrue("The bin start value should be inclusive", bin.start <= i) + assertTrue("The bin end value should be exclusive", i < bin.end) + } + } + + @Test + fun testEmptyHistogram() { + val emptyProto = Histogram(1).toProto() + assertEquals("There should be no samples in an empty histogram", 0, emptyProto.totalCount) + } +} \ No newline at end of file diff --git a/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java b/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java index 674a3ab..f68137a 100644 --- a/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java +++ b/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java
@@ -101,6 +101,7 @@ DisposerExplorer.visitTree(disposable -> { if (disposable.getClass().getName().equals("com.android.tools.idea.adb.AdbService") || disposable.getClass().getName().equals("com.android.tools.idea.adb.AdbOptionsService") || + disposable.getClass().getName().startsWith("com.android.tools.analytics.HighlightingStats") || (disposable instanceof ProjectImpl && (((ProjectImpl)disposable).isDefault() || ((ProjectImpl)disposable).isLight())) || disposable.toString().startsWith("services of " + ProjectImpl.class.getName()) || (disposable instanceof Module && ((Module)disposable).getName().equals(LightProjectDescriptor.TEST_MODULE_NAME)) || diff --git a/android/intellij.android.core.iml b/android/intellij.android.core.iml index 3772637..fca1944 100755 --- a/android/intellij.android.core.iml +++ b/android/intellij.android.core.iml
@@ -19,6 +19,9 @@ <orderEntry type="module" module-name="intellij.platform.debugger.impl" /> <orderEntry type="module" module-name="intellij.java.execution" /> <orderEntry type="module" module-name="intellij.platform.smRunner" /> + <orderEntry type="module" module-name="intellij.platform.core" /> + <orderEntry type="module" module-name="analytics" /> + <orderEntry type="module" module-name="analytics-publisher" /> <orderEntry type="module" module-name="intellij.junit" /> <orderEntry type="module" module-name="intellij.java.ui" /> <orderEntry type="module" module-name="intellij.json" />
diff --git a/android/lint_baseline.xml b/android/lint_baseline.xml index 70ed088..8ee66ac 100755 --- a/android/lint_baseline.xml +++ b/android/lint_baseline.xml
@@ -14,7 +14,7 @@ message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."> <location file="src/com/android/tools/idea/run/LaunchTaskRunner.java" - line="230"/> + line="239"/> </issue> <issue @@ -54,7 +54,7 @@ message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> <location file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" - line="613"/> + line="614"/> </issue> <issue @@ -62,15 +62,7 @@ message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> <location file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" - line="752"/> - </issue> - - <issue - id="JbUiStored" - message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> - <location - file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" - line="917"/> + line="753"/> </issue> <issue @@ -86,7 +78,15 @@ message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> <location file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" - line="1382"/> + line="919"/> + </issue> + + <issue + id="JbUiStored" + message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> + <location + file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" + line="1383"/> </issue> <issue @@ -278,7 +278,7 @@ message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> <location file="src/com/android/tools/idea/rendering/GutterIconRenderer.java" - line="89"/> + line="58"/> </issue> <issue @@ -286,7 +286,7 @@ message="Do not store `JBUI.scale` scaled results in fields; this will not work correctly on dynamic theme or font size changes"> <location file="src/com/android/tools/idea/rendering/GutterIconRenderer.java" - line="90"/> + line="59"/> </issue> <issue @@ -541,8 +541,16 @@ id="VisibleForTests" message="This method should only be accessed from tests or within private scope"> <location + file="src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java" + line="52"/> + </issue> + + <issue + id="VisibleForTests" + message="This method should only be accessed from tests or within private scope"> + <location file="src/com/android/tools/idea/gradle/project/sync/setup/module/dependency/DependenciesExtractor.java" - line="119"/> + line="79"/> </issue> <issue @@ -550,7 +558,7 @@ message="This method should only be accessed from tests or within private scope"> <location file="src/com/android/tools/idea/gradle/project/build/invoker/GradleTasksExecutorImpl.java" - line="381"/> + line="387"/> </issue> <issue @@ -606,7 +614,7 @@ message="Use `Double.valueOf(COLUMN_COUNT)` instead"> <location file="src/com/android/tools/idea/ui/resourcechooser/ColorPicker.java" - line="985"/> + line="986"/> </issue> <issue @@ -712,7 +720,7 @@ file="src/com/android/tools/idea/navigator/nodes/apk/ndk/NdkSourceNode.java" line="99"/> </issue> - + <issue id="FileComparisons" message="Do not compare java.io.File with `equals` or `==`: will not work correctly on case insensitive file systems! See `go/files-howto`."> @@ -774,7 +782,7 @@ message="Use `whenCompleteAsync` overload with an explicit Executor instead. See `go/do-not-freeze`."> <location file="src/com/android/tools/idea/rendering/RenderTask.java" - line="775"/> + line="802"/> </issue> <issue @@ -786,3 +794,31 @@ </issue> </issues> +<?xml version="1.0" encoding="UTF-8"?> +<issues format="5" by="lint 4.1.0-dev"> + + <issue + id="LintBaseline" + severity="Information" + message="1 errors/warnings were listed in the baseline file (../../../../../../../../../../../testlogs/tools/adt/idea/android/intellij.android.core_lint_test/test.outputs/lint_baseline.xml) but not found in the project; perhaps they have been fixed? Unmatched issue types: FileComparisons" + category="Lint" + priority="10" + summary="Baseline Issues" + explanation="Lint can be configured with a "baseline"; a set of current issues found in a codebase, which future runs of lint will silently ignore. Only new issues not found in the baseline are reported.

Note that while opening files in the IDE, baseline issues are not filtered out; the purpose of baselines is to allow you to get started using lint and break the build on all newly introduced errors, without having to go back and fix the entire codebase up front. However, when you open up existing files you still want to be aware of and fix issues as you come across them.

This issue type is used to emit two types of informational messages in reports: first, whether any issues were filtered out so you don't have a false sense of security if you forgot that you've checked in a baseline file, and second, whether any issues in the baseline file appear to have been fixed such that you can stop filtering them out and get warned if the issues are re-introduced."> + <location + file="../../../../../../../../../../../testlogs/tools/adt/idea/android/intellij.android.core_lint_test/test.outputs/lint_baseline.xml"/> + </issue> + + <issue + id="LintBaseline" + severity="Information" + message="85 errors and 14 warnings were filtered out because they are listed in the baseline file, ../../../../../../../../../../../testlogs/tools/adt/idea/android/intellij.android.core_lint_test/test.outputs/lint_baseline.xml
" + category="Lint" + priority="10" + summary="Baseline Issues" + explanation="Lint can be configured with a "baseline"; a set of current issues found in a codebase, which future runs of lint will silently ignore. Only new issues not found in the baseline are reported.

Note that while opening files in the IDE, baseline issues are not filtered out; the purpose of baselines is to allow you to get started using lint and break the build on all newly introduced errors, without having to go back and fix the entire codebase up front. However, when you open up existing files you still want to be aware of and fix issues as you come across them.

This issue type is used to emit two types of informational messages in reports: first, whether any issues were filtered out so you don't have a false sense of security if you forgot that you've checked in a baseline file, and second, whether any issues in the baseline file appear to have been fixed such that you can stop filtering them out and get warned if the issues are re-introduced."> + <location + file="../../../../../../../../../../../testlogs/tools/adt/idea/android/intellij.android.core_lint_test/test.outputs/lint_baseline.xml"/> + </issue> + +</issues>
diff --git a/android/src/META-INF/android-plugin.xml b/android/src/META-INF/android-plugin.xml index 3fc1925..a39077d 100644 --- a/android/src/META-INF/android-plugin.xml +++ b/android/src/META-INF/android-plugin.xml
@@ -39,7 +39,11 @@ <xi:include href="/com/android/tools/idea/lint/android-lint.xml" xpointer="xpointer(/idea-plugin/*)"/> <resource-bundle>messages.AndroidBundle</resource-bundle> - + <application-components> + <component> + <implementation-class>com.android.tools.analytics.HighlightingStats</implementation-class> + </component> + </application-components> <project-components> <component> <implementation-class>org.jetbrains.android.AndroidProjectComponent</implementation-class> @@ -315,6 +319,7 @@ </extensions> <extensions defaultExtensionNs="com.intellij"> + <ApplicationLoadListener implementation="com.android.tools.idea.startup.AndroidStudioInitializer$AndroidStudioLoadListener"/> <fileBasedIndex implementation="com.android.tools.idea.model.AndroidManifestIndex"/> <projectOpenProcessor implementation="com.android.tools.idea.gradle.project.AndroidGradleProjectOpenProcessor" id="android-gradle" order="before gradle" /> <projectViewPane implementation="com.android.tools.idea.navigator.AndroidProjectViewPane"/>
diff --git a/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java b/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java index b1bd623..a958b13 100755 --- a/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java +++ b/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java
@@ -16,6 +16,7 @@ package com.android.tools.idea.diagnostics; import com.android.tools.analytics.AnalyticsSettings; +import com.android.tools.analytics.HistogramUtil; import com.android.tools.analytics.UsageTracker; import com.android.tools.idea.diagnostics.crash.StudioCrashReporter; import com.android.tools.idea.diagnostics.hprof.action.AnalysisRunnable; @@ -95,7 +96,6 @@ import com.intellij.openapi.util.SystemInfoRt; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.analytics.HistogramUtil; import com.intellij.util.messages.MessageBusConnection; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;
diff --git a/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java b/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java new file mode 100644 index 0000000..1deb9c1 --- /dev/null +++ b/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java
@@ -0,0 +1,195 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.idea.startup; + +import com.android.tools.analytics.AnalyticsPublisher; +import com.android.tools.analytics.AnalyticsSettings; +import com.android.tools.analytics.AnalyticsSettingsData; +import com.android.tools.analytics.HighlightingStats; +import com.android.tools.analytics.StudioUpdateAnalyticsUtil; +import com.android.tools.analytics.UsageTracker; +import com.android.utils.ILogger; +import com.intellij.analytics.AndroidStudioAnalytics; +import com.intellij.concurrency.JobScheduler; +import com.intellij.ide.ConsentOptionsProvider; +import com.intellij.internal.statistic.persistence.UsageStatisticsPersistenceComponent; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationInfo; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class AndroidStudioAnalyticsImpl extends AndroidStudioAnalytics { + private ILogger androidLogger; + + @Override + public boolean isAllowed() { + // As we cannot control when IJ calls into this code, we need to load the AnalyticsSettings if + // we're not initialized yet, to ensure we properly return opt-in status. + if (!AnalyticsSettings.getInitialized()) { + Application application = ApplicationManager.getApplication(); + if (application != null && application.isUnitTestMode()) { + AnalyticsSettingsData analyticsSettings = new AnalyticsSettingsData(); + analyticsSettings.setOptedIn(false); + AnalyticsSettings.setInstanceForTest(analyticsSettings); + } else { + AnalyticsSettings.initialize(getAndroidLogger()); + } + } + return AnalyticsSettings.getOptedIn(); + + } + + + @Override + public void recordHighlightingLatency(Document document, long latencyMs) { + HighlightingStats.INSTANCE.recordHighlightingLatency(document, latencyMs); + } + + @Override + public void logUpdateDialogOpenManually(@NotNull String newBuild) { + StudioUpdateAnalyticsUtil.logUpdateDialogOpenManually(newBuild); + } + + @Override + public void logNotificationShown(@NotNull String newBuild) { + StudioUpdateAnalyticsUtil.logNotificationShown(newBuild); + } + + @Override + public void logClickNotification(@NotNull String newBuild) { + StudioUpdateAnalyticsUtil.logClickNotification(newBuild); + } + + @Override + public void logUpdateDialogOpenFromNotification(@NotNull String newBuild) { + StudioUpdateAnalyticsUtil.logUpdateDialogOpenFromNotification(newBuild); + } + + @Override + public void logClickIgnore(String newBuild) { + StudioUpdateAnalyticsUtil.logClickIgnore(newBuild); + } + + @Override + public void logClickLater(String newBuild) { + StudioUpdateAnalyticsUtil.logClickLater(newBuild); + } + + @Override + public void logDownloadSuccess(String newBuild) { + StudioUpdateAnalyticsUtil.logDownloadSuccess(newBuild); + } + + @Override + public void logDownloadFailure(String newBuild) { + StudioUpdateAnalyticsUtil.logDownloadFailure(newBuild); + } + + @Override + public void updateAndroidStudioMetrics() { + updateAndroidStudioMetrics(getConsentOptionsProvider().isSendingUsageStatsAllowed()); + } + + private @Nullable ConsentOptionsProvider getConsentOptionsProvider() { + return UsageStatisticsPersistenceComponent.getConsentOptionsProvider(); + } + + private void updateAndroidStudioMetrics(boolean allowed) { + + // Update the settings & tracker based on allowed state, will initialize on first call. + boolean updated = false; + try { + if (allowed == AnalyticsSettings.getOptedIn()) { + updated = false; + } else { + AnalyticsSettings.setOptedIn(allowed); + AnalyticsSettings.saveSettings(); + updated = true; + } + } catch (IOException e) { + getAndroidLogger().error(e, "Unable to update analytics settings"); + } + if (updated) { + initializeAndroidStudioUsageTrackerAndPublisher(); + } + } + + @Override + public void initializeAndroidStudioUsageTrackerAndPublisher() { + ILogger logger = getAndroidLogger(); + + ScheduledExecutorService scheduler = JobScheduler.getScheduler(); + AnalyticsSettings.initialize(logger, scheduler); + + try { + // If AnalyticsSettings and IJ opt-in status disagree, then we assume IJ is correct. + // This catches cornercases such as manual modifications as well as deal with the + // incorrect rename of "hasOptedIn" to "optedIn" in some early 3.3 canary builds. + boolean ijOptedIn = getConsentOptionsProvider().isSendingUsageStatsAllowed(); + if (AnalyticsSettings.getOptedIn() != ijOptedIn) { + AnalyticsSettings.setOptedIn(ijOptedIn); + AnalyticsSettings.saveSettings(); + } + UsageTracker.initialize(scheduler); + } catch (Exception e) { + logger.warning("Unable to initialize analytics tracker: " + e.getMessage()); + return; + } + // Update usage tracker maximums for long-lived process. + UsageTracker.setMaxJournalTime(10, TimeUnit.MINUTES); + UsageTracker.setMaxJournalSize(1000); + + ApplicationInfo application = ApplicationInfo.getInstance(); + AnalyticsPublisher.updatePublisher(logger, scheduler, application.getStrictVersion()); + } + + private ILogger getAndroidLogger() { + if (androidLogger == null) { + Logger intelliJLogger = Logger.getInstance("#com.intellij.internal.statistic.persistence.UsageStatisticsPersistenceComponent"); + // Create logger & scheduler based on IntelliJ/ADT helpers. + androidLogger = new ILogger() { + @Override + public void error(@com.android.annotations.Nullable Throwable t, + @com.android.annotations.Nullable String msgFormat, + Object... args) { + intelliJLogger.error(String.format(msgFormat, args), t); + } + + @Override + public void warning(String msgFormat, Object... args) { + intelliJLogger.warn(String.format(msgFormat, args)); + } + + @Override + public void info(String msgFormat, Object... args) { + intelliJLogger.info(String.format(msgFormat, args)); + } + + @Override + public void verbose(String msgFormat, Object... args) { + info(msgFormat, args); + } + }; + } + return androidLogger; + } +} diff --git a/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java b/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java index 0843092..e42abb2 100644 --- a/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java +++ b/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java
@@ -33,11 +33,13 @@ import com.android.tools.idea.testartifacts.junit.AndroidJUnitConfigurationType; import com.android.tools.idea.ui.resourcemanager.actions.ShowFileInResourceManagerAction; import com.google.wireless.android.sdk.stats.AndroidStudioEvent; +import com.intellij.analytics.AndroidStudioAnalytics; import com.intellij.concurrency.JobScheduler; import com.intellij.execution.actions.RunConfigurationProducer; import com.intellij.execution.configurations.ConfigurationType; import com.intellij.execution.junit.JUnitConfigurationProducer; import com.intellij.execution.junit.JUnitConfigurationType; +import com.intellij.ide.ApplicationLoadListener; import com.intellij.ide.fileTemplates.FileTemplate; import com.intellij.ide.fileTemplates.FileTemplateManager; import com.intellij.ide.plugins.PluginManagerCore; @@ -50,6 +52,7 @@ import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.PreloadingActivity; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.HighlighterColors; import com.intellij.openapi.editor.XmlHighlighterColors; @@ -69,7 +72,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer; import org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer; -import org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer; /** * Performs Android Studio specific initialization tasks that are build-system-independent. @@ -79,6 +81,15 @@ * </p> */ public class AndroidStudioInitializer implements ActionConfigurationCustomizer { + + public static class AndroidStudioLoadListener implements ApplicationLoadListener { + + @Override + public void beforeApplicationLoaded(@NotNull Application application, @NotNull String configPath) { + AndroidStudioAnalytics.initialize(new AndroidStudioAnalyticsImpl()); + } + } + @Override public void customize(@NotNull ActionManager actionManager) { checkInstallation(); @@ -121,7 +132,7 @@ * sets up collection of Android Studio specific analytics. */ private static void setupAnalytics() { - UsageStatisticsPersistenceComponent.getInstance().initializeAndroidStudioUsageTrackerAndPublisher(); + AndroidStudioAnalytics.getInstance().initializeAndroidStudioUsageTrackerAndPublisher(); // If the user hasn't opted in, we will ask IJ to check if the user has // provided a decision on the statistics consent. If the user hasn't made a diff --git a/android/src/com/android/tools/idea/stats/CompletionStats.kt b/android/src/com/android/tools/idea/stats/CompletionStats.kt index b0ba784..e90b101 100644 --- a/android/src/com/android/tools/idea/stats/CompletionStats.kt +++ b/android/src/com/android/tools/idea/stats/CompletionStats.kt
@@ -16,6 +16,7 @@ package com.android.tools.idea.stats import com.android.tools.analytics.UsageTracker +import com.android.tools.analytics.toProto import com.android.tools.idea.stats.CompletionStats.reportCompletionStats import com.google.wireless.android.sdk.stats.AndroidStudioEvent import com.google.wireless.android.sdk.stats.EditorCompletionStats @@ -30,7 +31,6 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupActivity -import com.intellij.util.analytics.toProto import org.HdrHistogram.SingleWriterRecorder import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap
diff --git a/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt b/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt index 48aa3c4..9c1d058 100644 --- a/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt +++ b/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt
@@ -16,6 +16,7 @@ package com.android.tools.idea.stats import com.android.tools.analytics.UsageTracker +import com.android.tools.analytics.toProto import com.android.tools.idea.stats.TypingLatencyTracker.reportTypingLatency import com.google.wireless.android.sdk.stats.AndroidStudioEvent import com.google.wireless.android.sdk.stats.EditorFileType @@ -23,7 +24,6 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.LatencyListener import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.util.analytics.toProto import org.HdrHistogram.SingleWriterRecorder import java.util.concurrent.ConcurrentHashMap
diff --git a/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt b/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt index 4ccd950..29fbd43 100644 --- a/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt +++ b/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt
@@ -22,6 +22,8 @@ import com.google.common.collect.Sets import com.google.common.truth.Truth.assertThat import com.intellij.analysis.AnalysisScope +import com.intellij.analytics.AndroidStudioAnalytics +import com.intellij.analytics.NullAndroidStudioAnalytics import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo import com.intellij.codeInsight.intention.IntentionAction @@ -85,6 +87,7 @@ myModule = moduleFixtureBuilder.fixture!!.module AndroidLintInspectionBase.setRegisterDynamicToolsFromTests(false) fixture.allowTreeAccessForAllFiles() + AndroidStudioAnalytics.initialize(NullAndroidStudioAnalytics()); } override fun tearDown() { diff --git a/studio-updater/BUILD b/studio-updater/BUILD new file mode 100644 index 0000000..eabed26 --- /dev/null +++ b/studio-updater/BUILD
@@ -0,0 +1,63 @@ +load("//tools/base/bazel:bazel.bzl", "iml_module") +load("//tools/base/bazel:coverage.bzl", "coverage_java_test") + +# managed by go/iml_to_build +iml_module( + name = "intellij.android.updater.studio-updater", + srcs = ["src"], + iml_files = ["intellij.android.updater.studio-updater.iml"], + # do not sort: must match IML order + test_runtime_deps = [ + "//tools/idea/xml/dom-openapi:intellij.xml.dom", + "//tools/idea/platform/testRunner:intellij.platform.testRunner", + "//tools/idea/xml/xml-structure-view-impl:intellij.xml.structureView.impl", + "//tools/idea/xml/dom-impl:intellij.xml.dom.impl", + "//tools/idea/spellchecker:intellij.spellchecker", + "//tools/idea/platform/lvcs-impl:intellij.platform.lvcs.impl", + "//tools/idea/platform/testFramework/extensions:intellij.platform.testExtensions", + "//tools/idea/platform/statistics/devkit:intellij.platform.statistics.devkit", + "//tools/idea/platform/credential-store:intellij.platform.credentialStore", + "//tools/idea/images:intellij.platform.images", + "//tools/idea/platform/external-system-impl:intellij.platform.externalSystem.impl", + "//tools/idea/platform/built-in-server:intellij.platform.builtInServer.impl", + "//tools/idea/platform/tasks-platform-impl:intellij.platform.tasks.impl", + "//tools/idea/json:intellij.json", + "//tools/idea/.idea/libraries:delight-rhino-sandbox", + "//tools/idea/.idea/libraries:rhino", + "//tools/idea/.idea/libraries:netty-handler-proxy", + "//tools/idea/.idea/libraries:javassist", + "//tools/idea/platform/diagnostic:intellij.platform.diagnostic", + "//tools/idea/.idea/libraries:error-prone-annotations", + "//tools/idea/.idea/libraries:javax.activation", + "//tools/idea/.idea/libraries:jaxb-api", + "//tools/idea/.idea/libraries:jaxb-runtime", + ], + test_srcs = ["testSrc"], + test_tags = ["manual"], # Tested via the integration_test target below + visibility = ["//visibility:public"], + runtime_deps = ["//prebuilts/tools/common/m2/repository/it/unimi/dsi/fastutil/7.2.1:jar"], + # do not sort: must match IML order + deps = [ + "//tools/idea/.idea/libraries:JUnit4[test]", + "//tools/idea/.idea/libraries:assertJ[test]", + "//tools/base/common:studio.android.sdktools.common[module]", + "//tools/analytics-library/shared:analytics-shared[module]", + "//tools/analytics-library/tracker:analytics-tracker[module]", + "//tools/idea/.idea/libraries:studio-analytics-proto", + "//tools/idea/.idea/libraries:protobuf", + "//tools/base/testutils:studio.android.sdktools.testutils[module, test]", + "//tools/idea/.idea/libraries:Guava[test]", + "//tools/idea/.idea/libraries:kotlin-stdlib-jdk8", + "//tools/idea/updater:intellij.platform.updater[module]", + "//tools/idea/platform/util:intellij.platform.util[module, test]", + "//tools/idea/platform/testFramework:intellij.platform.testFramework[module, test]", + ], +) + +coverage_java_test( + name = "integration_test", + data = ["//tools/idea/updater:updater_deploy.jar"], + tags = ["no_test_windows"], # b/77288863 + test_class = "com.android.studio.updater.StudioPatchUpdaterIntegrationTest", + runtime_deps = [":intellij.android.updater.studio-updater_testlib"], +) diff --git a/studio-updater/intellij.android.updater.studio-updater.iml b/studio-updater/intellij.android.updater.studio-updater.iml new file mode 100644 index 0000000..fc1f809 --- /dev/null +++ b/studio-updater/intellij.android.updater.studio-updater.iml
@@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" scope="TEST" name="JUnit4" level="project" /> + <orderEntry type="library" scope="TEST" name="assertJ" level="project" /> + <orderEntry type="module" module-name="android.sdktools.common" /> + <orderEntry type="module" module-name="analytics-shared" /> + <orderEntry type="module" module-name="analytics-tracker" /> + <orderEntry type="library" name="studio-analytics-proto" level="project" /> + <orderEntry type="library" name="protobuf" level="project" /> + <orderEntry type="module" module-name="android.sdktools.testutils" scope="TEST" /> + <orderEntry type="library" scope="TEST" name="Guava" level="project" /> + <orderEntry type="library" name="kotlin-stdlib-jdk8" level="project" /> + <orderEntry type="module" module-name="intellij.platform.updater" /> + <orderEntry type="module" module-name="intellij.platform.util" scope="TEST" /> + <orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" /> + </component> +</module> \ No newline at end of file
diff --git a/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService b/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService new file mode 100644 index 0000000..6629830 --- /dev/null +++ b/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService
@@ -0,0 +1 @@ +com.studio.updater.StudioUpdaterService \ No newline at end of file
diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt new file mode 100644 index 0000000..b31ffac --- /dev/null +++ b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt
@@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.studio.updater + +import com.android.tools.analytics.UsageTracker +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.ProductDetails +import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent +import com.intellij.updater.OperationCancelledException +import com.intellij.updater.UpdaterUI +import com.intellij.updater.ValidationResult + +/** A delegating Updater UI that reports events to Android Studio analytics for opted-in users. */ +class StudioUpdaterAnalyticsReportingUI(private val myDelegate: UpdaterUI) : UpdaterUI { + + override fun setDescription(oldBuildDesc: String, newBuildDesc: String) { + UsageTracker.version = newBuildDesc + + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + productDetails = ProductDetails.newBuilder().apply { + product = ProductDetails.ProductKind.STUDIO_PATCH_UPDATER + version = newBuildDesc + }.build() + + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW + patch = StudioPatchUpdaterEvent.Patch.newBuilder().apply { + studioVersionFrom = oldBuildDesc + studioVersionTo = newBuildDesc + }.build() + }.build() + } + ) + myDelegate.setDescription(oldBuildDesc, newBuildDesc) + } + + override fun setDescription(text: String) { + myDelegate.setDescription(text) + } + + override fun startProcess(title: String) { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = toAnalytics(title) + }.build() + }) + myDelegate.startProcess(title) + } + + override fun setProgress(percentage: Int) { + myDelegate.setProgress(percentage) + } + + override fun setProgressIndeterminate() { + myDelegate.setProgressIndeterminate() + } + + @Throws(OperationCancelledException::class) + override fun checkCancelled() { + myDelegate.checkCancelled() + } + + override fun showError(message: String) { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.FATAL_ERROR_DIALOG_SHOW + }.build() + }) + myDelegate.showError(message) + } + + @Throws(OperationCancelledException::class) + override fun askUser(message: String) { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.RETRYABLE_ERROR_DIALOG_SHOW + }.build() + }) + myDelegate.askUser(message) + } + + @Throws(OperationCancelledException::class) + override fun askUser(validationResults: List<ValidationResult>): Map<String, ValidationResult.Option> { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.VALIDATION_PROBLEMS_DIALOG_SHOW + issueDialog = toAnalytics(validationResults) + }.build() + }) + val result = myDelegate.askUser(validationResults) + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.VALIDATION_PROBLEMS_DIALOG_CLOSE + issueDialogChoices = toAnalytics(result) + }.build() + }) + return result + } + + override fun bold(text: String): String { + return myDelegate.bold(text) + } +} diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt new file mode 100644 index 0000000..35b38f0 --- /dev/null +++ b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt
@@ -0,0 +1,153 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("StudioUpdaterAnalyticsUtil") + +package com.studio.updater + + +import com.android.tools.analytics.AnalyticsSettings +import com.android.tools.analytics.UsageTracker +import com.android.utils.StdLogger +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent +import com.intellij.updater.ValidationResult +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit + +fun logProcessStart() { + AnalyticsSettings.initialize(StdLogger(StdLogger.Level.VERBOSE)) + UsageTracker.initialize(ScheduledThreadPoolExecutor(0)) + UsageTracker.setMaxJournalTime(10, TimeUnit.MINUTES) + UsageTracker.maxJournalSize = 1000 + + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.START + }.build() + }) + // Ensure all events are flushed to disk before process exit. + Runtime.getRuntime().addShutdownHook(Thread(UsageTracker::deinitialize)) +} + +fun logProcessSuccess() { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.EXIT_OK + }.build() + }) +} + +fun logProcessAbort() { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.EXIT_ABORT + }.build() + }) +} + +fun logException() { + UsageTracker.log( + AndroidStudioEvent.newBuilder().apply { + kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER + studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply { + kind = StudioPatchUpdaterEvent.Kind.EXIT_EXCEPTION + }.build() + }) +} + +private fun toAnalytics(kind: ValidationResult.Kind): StudioPatchUpdaterEvent.IssueDialog.Issue.Kind { + return when (kind) { + ValidationResult.Kind.INFO -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.INFO + ValidationResult.Kind.CONFLICT -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.CONFLICT + ValidationResult.Kind.ERROR -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.ERROR + } +} + +private fun toAnalytics(action: ValidationResult.Action): StudioPatchUpdaterEvent.IssueDialog.Issue.Action { + return when (action) { + ValidationResult.Action.CREATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.CREATE + ValidationResult.Action.UPDATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.UPDATE + ValidationResult.Action.DELETE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.DELETE + ValidationResult.Action.NO_ACTION -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.NO_ACTION + ValidationResult.Action.VALIDATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.VALIDATE + } +} + +private fun toAnalytics(result: ValidationResult): StudioPatchUpdaterEvent.IssueDialog.Issue.Builder { + return StudioPatchUpdaterEvent.IssueDialog.Issue.newBuilder().apply { + action = toAnalytics(result.action) + kind = toAnalytics(result.kind) + result.options.forEach { addPresentedOption(toAnalytics(it)) } + } +} + +private fun toAnalytics(value: ValidationResult.Option): StudioPatchUpdaterEvent.ValidationOption { + return when (value) { + ValidationResult.Option.NONE -> StudioPatchUpdaterEvent.ValidationOption.NONE + ValidationResult.Option.IGNORE -> StudioPatchUpdaterEvent.ValidationOption.IGNORE + ValidationResult.Option.KEEP -> StudioPatchUpdaterEvent.ValidationOption.KEEP + ValidationResult.Option.REPLACE -> StudioPatchUpdaterEvent.ValidationOption.REPLACE + ValidationResult.Option.DELETE -> StudioPatchUpdaterEvent.ValidationOption.DELETE + ValidationResult.Option.KILL_PROCESS -> StudioPatchUpdaterEvent.ValidationOption.KILL_PROCESS + } +} + +fun logProcessFinish(result: Boolean) { + if (result) { + logProcessSuccess() + } + else { + logProcessAbort() + } +} + +internal fun toAnalytics(phase: String): StudioPatchUpdaterEvent.Kind { + return when (phase) { + "Extracting patch file...", "Extracting patch files..." -> StudioPatchUpdaterEvent.Kind.PHASE_EXTRACTING_PATCH_FILES + "Validating installation..." -> StudioPatchUpdaterEvent.Kind.PHASE_VALIDATING_INSTALLATION + "Backing up files..." -> StudioPatchUpdaterEvent.Kind.PHASE_BACKING_UP_FILES + "Preparing update..." -> StudioPatchUpdaterEvent.Kind.PHASE_PREPARING_UPDATE + "Applying patch..." -> StudioPatchUpdaterEvent.Kind.PHASE_APPLYING_PATCH + "Reverting..." -> StudioPatchUpdaterEvent.Kind.PHASE_REVERTING + "Cleaning up..." -> StudioPatchUpdaterEvent.Kind.PHASE_CLEANING_UP + else -> StudioPatchUpdaterEvent.Kind.PHASE_UNKNOWN + } +} + +internal fun toAnalytics(results: List<ValidationResult>): StudioPatchUpdaterEvent.IssueDialog { + return StudioPatchUpdaterEvent.IssueDialog.newBuilder().apply { + results.forEach { addIssue(toAnalytics(it)) } + }.build() +} + +internal fun toAnalytics(result: Map<String, ValidationResult.Option>): StudioPatchUpdaterEvent.IssueDialogChoices { + return StudioPatchUpdaterEvent.IssueDialogChoices.newBuilder().apply { + result.values.forEach { addChoice(validationToAnalytics(it)) } + }.build() +} + +private fun validationToAnalytics(value: ValidationResult.Option): StudioPatchUpdaterEvent.IssueDialogChoices.Choice { + return StudioPatchUpdaterEvent.IssueDialogChoices.Choice.newBuilder().apply { + chosenOption = toAnalytics(value) + }.build() +} \ No newline at end of file diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterService.java b/studio-updater/src/com/studio/updater/StudioUpdaterService.java new file mode 100644 index 0000000..cf18ef3 --- /dev/null +++ b/studio-updater/src/com/studio/updater/StudioUpdaterService.java
@@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.studio.updater; + +import com.intellij.updater.UpdaterUI; + +public class StudioUpdaterService extends UpdaterService { + @Override + public void logProcessStart() { + StudioUpdaterAnalyticsUtil.logProcessStart(); + } + + @Override + public UpdaterUI wrap(UpdaterUI ui) { + return new StudioUpdaterAnalyticsReportingUI(ui); + } + + @Override + public void logProcessFinish(boolean success) { + StudioUpdaterAnalyticsUtil.logProcessFinish(success); + } + + @Override + public void logException() { + StudioUpdaterAnalyticsUtil.logException(); + } +} diff --git a/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt b/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt new file mode 100644 index 0000000..ceb0d5f --- /dev/null +++ b/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt
@@ -0,0 +1,228 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.studio.updater + +import com.android.testutils.TestUtils +import com.google.common.collect.MoreCollectors +import com.google.wireless.android.play.playlog.proto.ClientAnalytics +import com.google.wireless.android.sdk.stats.AndroidStudioEvent +import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent +import com.intellij.openapi.util.SystemInfo +import com.intellij.testFramework.rules.TempDirectory +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.io.BufferedInputStream +import java.nio.charset.StandardCharsets +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.ArrayList +import java.util.HashMap + +/** + * Integration test for the studio updater. + * + * This is formulated to check the packaging of the updater that may be changed by intellij merges, + * hence uses the final binary, rather than being run as a unit test. + */ +@RunWith(JUnit4::class) +class StudioPatchUpdaterIntegrationTest { + + @get:Rule + var myTempDirectory = TempDirectory() + + private lateinit var java: Path + private lateinit var patchJar: Path + + enum class ExampleDirectory(private val files: Map<String, String>) { + V1(mapOf("removed" to "v1_removed_later", "changed" to "v1_changed")), + V2(mapOf("added" to "v2_added_since_v1", "changed" to "v2_changed")), + V3(mapOf("changed" to "v3_changed")); + + internal fun createExampleDir(tempDirectory: TempDirectory): Path { + val dir = tempDirectory.newFolder().toPath() + for ((path, content) in files) { + val file = dir.resolve(path) + Files.createDirectories(file.parent) + Files.write(file, setOf(content)) + } + // Sanity test + verifyDir(dir) + return dir + } + + internal fun verifyDir(dir: Path) { + val actual = readDir(dir) + assertEquals(files, actual) + } + + private fun readDir(dir: Path): Map<String, String> { + val actual = HashMap<String, String>() + Files.walkFileTree(dir, object : SimpleFileVisitor<Path>() { + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val filePath = dir.relativize(file).toString().replace(dir.fileSystem.separator, "/") + actual[filePath] = Files.readAllLines(file).joinToString("\n") + return FileVisitResult.CONTINUE + } + }) + return actual + } + + } + + @Before + fun createPatch() { + java = Paths.get(System.getProperty("java.home")).resolve("bin").resolve(if (SystemInfo.isWindows) "java.exe" else "java") + patchJar = myTempDirectory.newFolder("patch").toPath().resolve("patch.jar") + + // Build the patch + + val createPatcher = arrayOf(java.toString(), "-cp", updaterFullJar.toString(), UPDATER_MAIN_CLASS, "create", "v1", "v2", + ExampleDirectory.V1.createExampleDir(myTempDirectory).toString(), + ExampleDirectory.V2.createExampleDir(myTempDirectory).toString(), patchJar.toString(), "--strict") + runExpectingOk(createPatcher, mapOf()) + assertTrue(Files.isRegularFile(patchJar)) + } + + /** + * Smoke test for patch being correctly applied + */ + @Test + fun patchApplicationSmokeTest() { + val analyticsHome = createAnalyticsHome() + val env = mapOf("ANDROID_SDK_HOME" to analyticsHome.toString()) + + // When V1 to V2 patch applied to V1 + val dir = ExampleDirectory.V1.createExampleDir(myTempDirectory) + val applyPatch = arrayOf(java.toString(), "-cp", patchJar.toString(), UPDATER_MAIN_CLASS, "apply", dir.toString()) + // Patcher should succeed. + runExpectingOk(applyPatch, env) + + // Result should be V2 + ExampleDirectory.V2.verifyDir(dir) + + // Check the events produced. This is kept as an integration test to also cover the packaging of the updater binary. + val events = readEvents(analyticsHome) + assertEquals(8, events.size.toLong()) + for (event in events) { + assertEquals(AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER, event.kind) + } + val expectedEventSequence = listOf( + StudioPatchUpdaterEvent.Kind.START, + StudioPatchUpdaterEvent.Kind.PHASE_EXTRACTING_PATCH_FILES, + StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW, + StudioPatchUpdaterEvent.Kind.PHASE_VALIDATING_INSTALLATION, + StudioPatchUpdaterEvent.Kind.PHASE_BACKING_UP_FILES, + StudioPatchUpdaterEvent.Kind.PHASE_APPLYING_PATCH, + StudioPatchUpdaterEvent.Kind.PHASE_CLEANING_UP, + StudioPatchUpdaterEvent.Kind.EXIT_OK + ) + assertEquals(expectedEventSequence, events.map { it.studioPatchUpdaterEvent.kind }) + + val details = events.map { it.studioPatchUpdaterEvent }.find { it.kind == StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW }!! + assertEquals("v1", details.patch.studioVersionFrom) + assertEquals("v2", details.patch.studioVersionTo) + } + + /** + * Smoke test for patch failing to be applied. + */ + @Test + fun patchApplicationFailureTest() { + + // When V1 to V2 patch applied to some other version + val dir = ExampleDirectory.V3.createExampleDir(myTempDirectory) + val applyPatch = arrayOf(java.toString(), "-cp", patchJar.toString(), UPDATER_MAIN_CLASS, "apply", dir.toString()) + // Patcher should fail + runExpectingError(applyPatch, mapOf()) + + // Version should not be corrupted. + ExampleDirectory.V3.verifyDir(dir) + } + + private fun readEvents(analyticsHome: Path): List<AndroidStudioEvent> { + // Check the analytics were written. + val spool = analyticsHome.resolve("metrics/spool") + val trackFile: Path = Files.list(spool).use { paths -> paths.collect(MoreCollectors.onlyElement<Path>()) } + val events = ArrayList<AndroidStudioEvent>() + + BufferedInputStream(Files.newInputStream(trackFile)).use { inputStream -> + // read all LogEvents from the trackFile. + while (true) { + val event = ClientAnalytics.LogEvent.parseDelimitedFrom(inputStream) ?: break + val studioEvent = AndroidStudioEvent.parseFrom(event.sourceExtension) + events.add(studioEvent) + println(studioEvent) + println("---") + } + } + return events + } + + private fun createAnalyticsHome(): Path { + val path = myTempDirectory.newFolder().toPath() + val json = "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }" + Files.write( + path.resolve("").resolve("analytics.settings"), + json.toByteArray(StandardCharsets.UTF_8)) + return path + } + + // See UpdateInstaller.UPDATER_MAIN_CLASS + private val UPDATER_MAIN_CLASS = "com.intellij.updater.Runner" + + private fun runExpectingOk(command: Array<String>, env: Map<String, String>) { + val returnValue = run(command, env) + assertEquals("Expected command to run successfully " + command.joinToString(" "), 0, returnValue.toLong()) + } + + private fun runExpectingError(command: Array<String>, env: Map<String, String>) { + val returnValue = run(command, env) + assertNotEquals("Expected command to fail " + command.joinToString(" "), 0, returnValue.toLong()) + } + + private fun run(createPatcher: Array<String>, env: Map<String, String>): Int { + val builder = ProcessBuilder(*createPatcher) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + builder.environment().putAll(env) + return builder.start().waitFor() + } + + private val updaterFullJar: Path + get() { + val root = TestUtils.getWorkspaceRoot().toPath() + val bazelDeployJar = root.resolve("tools/idea/updater/updater_deploy.jar") + if (Files.isRegularFile(bazelDeployJar)) { + return bazelDeployJar + } + val antJar = root.resolve("tools/idea/out/studio/artifacts/updater-full.jar") + if (Files.isRegularFile(antJar)) { + return antJar + } + throw RuntimeException("Unable to find updater deploy jar. Perhaps run cd tools/idea && ant fullupdater") + } + +} \ No newline at end of file